/**
 * Project Name:kany-utils
 * File Name:Section.java
 * Package Name:me.kany.utils.config.ini
 * Date:2016-1-7上午11:01:21
 * Copyright (c) 2016, Jason.Wang All Rights Reserved.
 *
 */

package me.kany.utils.config.ini.section;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import me.kany.utils.config.ini.comment.Comment;
import me.kany.utils.config.ini.line.Line;
import me.kany.utils.config.ini.option.Option;
import me.kany.utils.config.ini.option.OptionFormat;

/**
 * ClassName:Section <br/>
 * Function: INI文件读取操作的配置文件类，节点. <br/>
 * Date: 2016-1-7 上午11:01:21 <br/>
 * 
 * @author Jason.Wang
 * @version
 * @since JDK 1.7
 * @see
 */
public class Section {

    private String name;
    private Map<String, Option> options;
    private List<Line> lines;
    private char[] optionDelims;
    private char[] optionDelimsSorted;
    private char[] commentDelims;
    private char[] commentDelimsSorted;
    private boolean isCaseSensitive;
    private OptionFormat optionFormat;

	private static final char[] DEFAULT_OPTION_DELIMS = new char[] { '=', ':' };
	private static final char[] DEFAULT_COMMENT_DELIMS = new char[] { '#', ';' };
	private static final char[] OPTION_DELIMS_WHITESPACE = new char[] { ' ', '\t' };
	private static final boolean DEFAULT_CASE_SENSITIVITY = false;
	public static final String DEFAULT_OPTION_FORMAT = "%s %s %s";

	public static final char HEADER_START = '[';
	public static final char HEADER_END = ']';
	private static final int NAME_MAXLENGTH = 1024;
	private static final char[] INVALID_NAME_CHARS = { HEADER_START, HEADER_END };
	
	private static final Line BLANK_LINE = new Line() {
        public String toString() { return ""; }
    };

    /**
     * Constructs a new section.
     *
     * @param name the section's name
     * @throws IllegalArgumentException the section's name is illegal
     */
    public Section(String name) {
        this(name, null);
    }

    /**
     * Constructs a new section, specifying case-sensitivity.
     *
     * @param name the section's name
     * @param isCaseSensitive option names are case-sensitive if this is true
     * @throws IllegalArgumentException the section's name is illegal
     */
    public Section(String name, boolean isCaseSensitive) {
        this(name, null, isCaseSensitive);
    }

    /**
     * Constructs a new section, defining comment delimiters.
     *
     * @param name the section's name
     * @param delims an array of characters to be recognized as starters of
     *    comment lines; the first of them will be used for newly created
     *    comments
     * @throws IllegalArgumentException the section's name is illegal
     */
    public Section(String name, char[] delims) {
        this(name, delims, DEFAULT_CASE_SENSITIVITY);
    }

    /**
     * Constructs a new section, defining comment delimiters.
     *
     * @param name the section's name
     * @param delims an array of characters to be recognized as starters of
     *    comment lines; the first of them will be used for newly created
     *    comments
     * @param isCaseSensitive option names are case-sensitive if this is true
     * @throws IllegalArgumentException the section's name is illegal
     */
    public Section(String name, char[] delims, boolean isCaseSensitive) {
        if (!validName(name)) {
            throw new IllegalArgumentException("Illegal section name:" + name);
        }
        this.name = name;
        this.isCaseSensitive = isCaseSensitive;
        this.options = new HashMap<String, Option>();
        this.lines = new LinkedList<Line>();
        this.optionDelims = DEFAULT_OPTION_DELIMS;
        this.commentDelims = (delims == null ? DEFAULT_COMMENT_DELIMS : delims);
        this.optionFormat = new OptionFormat(DEFAULT_OPTION_FORMAT);
        // sorting so we can later use binary search
        this.optionDelimsSorted = new char[this.optionDelims.length];
        System.arraycopy(this.optionDelims, 0, this.optionDelimsSorted, 0, this.optionDelims.length);
        this.commentDelimsSorted = new char[this.commentDelims.length];
        System.arraycopy(this.commentDelims, 0, this.commentDelimsSorted, 0, this.commentDelims.length);
        Arrays.sort(this.optionDelimsSorted);
        Arrays.sort(this.commentDelimsSorted);
    }

    /**
     * Sets the option format for this section to the given string. Options
     * in this section will be rendered according to the given format
     * string. The string must contain <code>%s</code> three times, these
     * will be replaced with the option name, the option separator and the
     * option value in this order. Literal percentage signs must be escaped
     * by preceding them with another percentage sign (i.e., <code>%%</code>
     * corresponds to one percentage sign). The default format string is
     * <code>"%s %s %s"</code>.
     *
     * Option formats may look like format strings as supported by Java 1.5,
     * but the string is in fact parsed in a custom fashion to guarantee
     * backwards compatibility. So don't try clever stuff like using format
     * conversion types other than <code>%s</code>.
     *
     * @param formatString a format string, containing <code>%s</code>
     *      exactly three times
     * @throws IllegalArgumentException if the format string is illegal
     */
    public void setOptionFormatString(String formatString) {
        this.setOptionFormat(new OptionFormat(formatString));
    }

    /**
     * Sets the option format for this section. Options will be rendered
     * according to the given format when printed.
     *
     * @param format a compiled option format
     */
    public void setOptionFormat(OptionFormat format) {
        this.optionFormat = format;
    }

    /**
     * Returns the names of all options in this section.
     *
     * @return list of names of this section's options in
     *      original/insertion order
     */
    public List<String> optionNames() {
        List<String> optNames = new LinkedList<String>();
        Iterator<Line> it = this.lines.iterator();
        while (it.hasNext()) {
            Line line = it.next();
            if (line instanceof Option) {
                optNames.add(((Option)line).name());
            }
        }
        return optNames;
    }

    /**
     * Checks whether a given option exists in this section.
     *
     * @param name the name of the option to test for
     * @return true if the option exists in this section
     */
    public boolean hasOption(String name) {
        return this.options.containsKey(normOption(name));
    }

    /**
     * Returns an option's value.
     *
     * @param option the name of the option
     * @return the requested option's value or <code>null</code> if no
     *      option with the specified name exists
     */
    public String get(String option) {
        String normed = normOption(option);
        if (hasOption(normed)) {
            return getOption(normed).value();
        }
        return null;
    }

    /**
     * Sets an option's value and creates the option if it doesn't exist.
     *
     * @param option the option's name
     * @param value the option's value
     * @throws IllegalArgumentException the option name is illegal,
     *      ie contains a '=' character or consists only of white space
     */
    public void set(String option, String value) {
        set(option, value, this.optionDelims[0]);
    }

    /**
     * Sets an option's value and creates the option if it doesn't exist.
     *
     * @param option the option's name
     * @param value the option's value
     * @param delim the delimiter between name and value for this option
     * @throws IllegalArgumentException the option name is illegal,
     *      ie contains a '=' character or consists only of white space
     */
    public void set(String option, String value, char delim) {
        String normed = normOption(option);
        if (hasOption(normed)) {
            getOption(normed).set(value);
        }
        else {
            // Option constructor might throw IllegalArgumentException
            Option opt = new Option(normed, value, delim, this.optionFormat);
            this.options.put(normed, opt);
            this.lines.add(opt);
        }
    }

    /**
     * Removes an option if it exists.
     *
     * @param option the name of the option
     * @return <code>true</code> if the option was actually removed
     */
    public boolean remove(String option) {
        String normed = normOption(option);
        if (hasOption(normed)) {
            this.lines.remove(getOption(normed));
            this.options.remove(normed);
            return true;
        }
        else {
            return false;
        }
    }

    /**
     * Adds a comment line to the end of this section. A comment spanning
     * several lines (ie with line breaks) will be split up, one comment
     * line for each line.
     *
     * @param comment the comment
     */
    public void addComment(String comment) {
        addComment(comment, this.commentDelims[0]);
    }

    /**
     * Adds a comment line to the end of this section. A comment spanning
     * several lines (ie with line breaks) will be split up, one comment
     * line for each line.
     *
     * @param comment the comment
     * @param delim the delimiter used to mark the start of this comment
     */
    public void addComment(String comment, char delim) {
        StringTokenizer st = new StringTokenizer(comment.trim(), NEWLINE_CHARS);
        while (st.hasMoreTokens()) {
            this.lines.add(new Comment(st.nextToken(), delim));
        }
    }
    private static final String NEWLINE_CHARS = "\n\r";

    /**
     * Adds a blank line to the end of this section.
     */
    public void addBlankLine() {
        this.lines.add(BLANK_LINE);
    }

    /**
     * Loads options from a reader into this instance. Will read from the
     * stream until it hits a section header, ie a '[' character, and resets
     * the reader to point to this character.
     *
     * @param reader where to read from
     * @throws IOException at an I/O problem
     */
    public void load(BufferedReader reader) throws IOException {
        while (reader.ready()) {
            reader.mark(NAME_MAXLENGTH);
            String line = reader.readLine().trim();

            // Check for section header
            if (line.length() > 0 && line.charAt(0) == HEADER_START) {
                reader.reset();
                return;
            }

            int delimIndex = -1;
            // blank line
            if (line.equals("")) {
                this.addBlankLine();
            }
            // comment line
            else if ((delimIndex = Arrays.binarySearch(this.commentDelimsSorted, line.charAt(0))) >= 0) {
                addComment(line.substring(1), this.commentDelimsSorted[delimIndex]);
            }
            // option line
            else {
                delimIndex = -1;
                int delimNum = -1;
                int lastSpaceIndex = -1;
                for (int i = 0, l = line.length(); i < l && delimIndex < 0; i++) {
                    delimNum = Arrays.binarySearch(this.optionDelimsSorted, line.charAt(i));
                    if (delimNum >= 0) {
                        delimIndex = i;
                    }
                    else {
                        boolean isSpace = Arrays.binarySearch(
                            OPTION_DELIMS_WHITESPACE, line.charAt(i)) >= 0;
                        if (!isSpace && lastSpaceIndex >= 0) {
                            break;
                        }
                        else if (isSpace) {
                            lastSpaceIndex = i;
                        }
                    }
                }
                // delimiter at start of line
                if (delimIndex == 0) {
                }
                // no delimiter found
                else if (delimIndex < 0) {
                    if (lastSpaceIndex < 0) {
                        this.set(line, "");
                    }
                    else {
                        this.set(line.substring(0, lastSpaceIndex)
                            , line.substring(lastSpaceIndex+1));
                    }
                }
                // delimiter found
                else {
                    this.set(line.substring(0, delimIndex)
                        , line.substring(delimIndex+1), line.charAt(delimIndex));
                }
            }
        }
    }

    /**
     * Prints this section to a print writer.
     *
     * @param writer where to write
     * @throws IOException at an I/O problem
     */
    public void save(PrintWriter writer) throws IOException {
        Iterator<Line> it = this.lines.iterator();
        while (it.hasNext()) {
            writer.println(it.next().toString());
        }
        if (writer.checkError()) {
            throw new IOException();
        }
    }

    /**
     * Returns an actual Option instance.
     *
     * @param option the name of the option, assumed to be normed already (!)
     * @return the requested Option instance
     * @throws NullPointerException if no option with the specified name exists
     */
    private Option getOption(String name) {
        return this.options.get(name);
    }

    /**
     * Returns the bracketed header of this section as appearing in an
     * actual INI file.
     *
     * @return the section's name in brackets
     */
    public String header() {
        return HEADER_START + this.name + HEADER_END;
    }

    /**
     * Checks a string for validity as a section name. It can't contain the
     * characters '[' and ']'. An empty string or one consisting only of
     * white space isn't allowed either.
     *
     * @param name the name to validate
     * @return true if the name validates as a section name
     */
    private static boolean validName(String name) {
        if (name.trim().equals("")) {
            return false;
        }
        for (int i = 0; i < INVALID_NAME_CHARS.length; i++) {
            if (name.indexOf(INVALID_NAME_CHARS[i]) >= 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * Normalizes an arbitrary string for use as an option name, ie makes
     * it lower-case (provided this section isn't case-sensitive) and trims
     * leading and trailing white space.
     *
     * @param name the string to be used as option name
     * @return a normalized option name
     */
    private String normOption(String name) {
        if (!this.isCaseSensitive) {
            name = name.toLowerCase();
        }
        return name.trim();
    }
}